// 
//  dmap-structure.vala
//  
//  Author:
//       Alexandre Rosenfeld <alexandre.rosenfeld@gmail.com>
//  
//  Copyright (c) 2011 Alexandre Rosenfeld
// 
//  This program is free software: you can redistribute it and/or modify
//  it under the terms of the GNU Lesser General Public License as published by
//  the Free Software Foundation, either version 3 of the License, or
//  (at your option) any later version.
// 
//  This program is distributed in the hope that it will be useful,
//  but WITHOUT ANY WARRANTY; without even the implied warranty of
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//  GNU Lesser General Public License for more details.
// 
//  You should have received a copy of the GNU Lesser General Public License
//  along with this program.  If not, see <http://www.gnu.org/licenses/>.

namespace mShare.DMAP {

	public enum Type {
		BYTE = 0x0001,
		SIGNED_INT = 0x0002,
		SHORT = 0x0003,
		INT = 0x0005,
		INT64 = 0x0007,
		STRING = 0x0009,
		DATE = 0x000A,
		VERSION = 0x000B,
		CONTAINER = 0x000C,
		POINTER = 0x002A
	}

	private static GLib.HashTable<string,unowned ContentCode?> cc_names;

	public struct ContentCode {
		public string code;
		public string? name;
		public Type type;

		public uint32 hash () {
			return this.code[0] |
			       this.code[1] << 8  |
			       this.code[2] << 16 |
			       this.code[3] << 24;
		}

		public const ContentCode[] CODES = {
			{"mdcl", "dmap.dictionary", Type.CONTAINER},
			{"mstt", "dmap.status", Type.INT},
			{"miid", "dmap.itemid", Type.INT},
			{"minm", "dmap.itemname", Type.STRING},
			{"mikd", "dmap.itemkind", Type.BYTE},
			{"mper", "dmap.persistentid", Type.INT64},
			{"mcon", "dmap.container", Type.CONTAINER},
			{"mcti", "dmap.containeritemid", Type.INT},
			{"mpco", "dmap.parentcontainerid", Type.INT},
			{"msts", "dmap.statusstring", Type.STRING},
			{"mimc", "dmap.itemcount", Type.INT},
			{"mctc", "dmap.containercount", Type.INT},
			{"mrco", "dmap.returnedcount", Type.INT},
			{"mtco", "dmap.specifiedtotalcount", Type.INT},
			{"mlcl", "dmap.listing", Type.CONTAINER},
			{"mlit", "dmap.listingitem", Type.CONTAINER},
			{"mbcl", "dmap.bag", Type.CONTAINER},
			{"msrv", "dmap.serverinforesponse", Type.CONTAINER},
			{"msau", "dmap.authenticationmethod", Type.BYTE},
			{"mslr", "dmap.loginrequired", Type.BYTE},
			{"mpro", "dmap.protocolversion", Type.VERSION},
			{"msal", "dmap.supportsautologout", Type.BYTE},
			{"msup", "dmap.supportsupdate", Type.BYTE},
			{"mspi", "dmap.supportspersistenids", Type.BYTE},
			{"msex", "dmap.supportsextensions", Type.BYTE},
			{"msbr", "dmap.supportsbrowse", Type.BYTE},
			{"msqy", "dmap.supportsquery", Type.BYTE},
			{"msix", "dmap.supportsindex", Type.BYTE},
			{"msrs", "dmap.supportsresolve", Type.BYTE},
			{"mstm", "dmap.timeoutinterval", Type.INT},
			{"msdc", "dmap.databasescount", Type.INT},
			{"mccr", "dmap.contentcodesresponse", Type.CONTAINER},
			{"mcnm", "dmap.contentcodesnumber", Type.INT},
			{"mcna", "dmap.contentcodesname", Type.STRING},
			{"mcty", "dmap.contentcodestype", Type.SHORT},
			{"mlog", "dmap.loginresponse", Type.CONTAINER},
			{"mlid", "dmap.sessionid", Type.INT},
			{"mupd", "dmap.updateresponse", Type.CONTAINER},
			{"musr", "dmap.serverrevision", Type.INT},
			{"muty", "dmap.updatetype", Type.BYTE},
			{"mudl", "dmap.deletedidlisting", Type.CONTAINER},
			{"msma", "dmap.speakermachineaddress", Type.INT},
			{"f?ch", "dmap.haschildcontainers", Type.BYTE},
			
			{"apro", "daap.protocolversion", Type.VERSION},
			{"avdb", "daap.serverdatabases", Type.CONTAINER},
			{"abro", "daap.databasebrowse", Type.CONTAINER},
			{"abal", "daap.browsealbumlisting", Type.CONTAINER},
			{"abar", "daap.browseartistlisting", Type.CONTAINER},
			{"abcp", "daap.browsecomposerlisting", Type.CONTAINER},
			{"abgn", "daap.browsegenrelisting", Type.CONTAINER},
			{"adbs", "daap.returndatabasesongs", Type.CONTAINER},
			{"asal", "daap.songalbum", Type.STRING},
			{"asai", "daap.songalbumid", Type.INT},
			{"asaa", "daap.songalbumartist", Type.STRING},
			{"asar", "daap.songartist", Type.STRING},
			{"asbt", "daap.songsbeatsperminute", Type.SHORT},
			{"asbr", "daap.songbitrate", Type.SHORT},
			{"ascm", "daap.songcomment", Type.STRING},
			{"asco", "daap.songcompliation", Type.BYTE},
			{"asda", "daap.songdateadded", Type.DATE},
			{"asdm", "daap.songdatemodified", Type.DATE},
			{"asdc", "daap.songdisccount", Type.SHORT},
			{"asdn", "daap.songdiscnumber", Type.SHORT},
			{"asdb", "daap.songdisabled", Type.BYTE},
			{"aseq", "daap.songeqpreset", Type.STRING},
			{"asfm", "daap.songformat", Type.STRING},
			{"asgn", "daap.songgenre", Type.STRING},
			{"asdt", "daap.songdescription", Type.STRING},
			{"asrv", "daap.songrelativevolume", Type.SIGNED_INT},
			{"assr", "daap.songsamplerate", Type.INT},
			{"assz", "daap.songsize", Type.INT},
			{"asst", "daap.songstarttime", Type.INT},
			{"assp", "daap.songstoptime", Type.INT},
			{"astm", "daap.songtime", Type.INT},
			{"astc", "daap.songtrackcount", Type.SHORT},
			{"astn", "daap.songtracknumber", Type.SHORT},
			{"asur", "daap.songuserrating", Type.BYTE},
			{"asyr", "daap.songyear", Type.SHORT},
			{"asdk", "daap.songdatakind", Type.BYTE},
			{"asul", "daap.songdataurl", Type.STRING},
			{"assu", "daap.sortalbum", Type.STRING},
			{"assa", "daap.sortartist", Type.STRING},
			{"aply", "daap.databaseplaylists", Type.CONTAINER},
			{"abpl", "daap.baseplaylist", Type.BYTE},
			{"apso", "daap.playlistsongs", Type.CONTAINER},
			{"prsv", "daap.resolve", Type.CONTAINER},
			{"arif", "daap.resolveinfo", Type.CONTAINER},
			{"msas", "daap.authentication.schemes", Type.BYTE},
			{"agrp", "daap.songgrouping", Type.STRING},
			{"agal", "daap.albumgrouping", Type.CONTAINER},
			{"ascp", "daap.songcomposer", Type.STRING},
			{"ppro", "dpap.protocolversion", Type.VERSION},
			{"pasp", "dpap.aspectratio", Type.STRING},
			{"pfdt", "dpap.filedata", Type.POINTER},
			{"picd", "dpap.creationdate", Type.INT},
			{"pimf", "dpap.imagefilename", Type.STRING},
			{"pfmt", "dpap.imageformat", Type.STRING},
			{"pifs", "dpap.imagefilesize", Type.INT},
			{"plsz", "dpap.imagelargefilesize", Type.INT},
			{"phgt", "dpap.imagepixelheight", Type.INT},
			{"pwth", "dpap.imagepixelwidth", Type.INT},
			{"prat", "dpap.imagerating", Type.INT},
			{"pcmt", "dpap.imagecomments", Type.STRING},
			{"pret", "dpap.pret", Type.STRING},
			{"aesv", "com.apple.itunes.music-sharing-version", Type.INT},
			{"aeHV", "com.apple.itunes.has-video", Type.BYTE},
			{"aeSP", "com.apple.itunes.smart-playlist", Type.BYTE},
			{"aePP", "com.apple.itunes.is-podcast-playlist", Type.BYTE},
			{"aePS", "com.apple.itunes.special-playlist", Type.BYTE},
			{"aeSG", "com.apple.itunes.saved-genius", Type.BYTE},
			{"aeMK", "com.apple.itunes.mediakind", Type.BYTE},
			{"aeFP", "com.apple.itunes.req-fplay", Type.BYTE},
		
			{"cmpa", "dacp.contentcontainer", Type.CONTAINER},
			{"cmnm", "dacp.contentname", Type.STRING},
			{"cmty", "dacp.contentvalue", Type.STRING},
			{"cmpy", "dacp.passguid", Type.INT64},
		
			/* DACP */
			{"caci", "dacp.controlint", Type.CONTAINER},
			{"caci", "dacp.playstatus", Type.BYTE},
			{"caci", "dacp.shufflestate", Type.BYTE},
			{"caci", "dacp.repeatstate", Type.BYTE},
			{"caas", "dacp.albumshuffle", Type.INT},
			{"caci", "dacp.albumrepeat", Type.INT},
			{"caia", "dacp.isavailiable", Type.BYTE},
			{"canp", "dacp.nowplaying", Type.INT64},
			{"cann", "dacp.nowplayingtrack", Type.STRING},
			{"cana", "dacp.nowplayingartist", Type.STRING},
			{"canl", "dacp.nowplayingalbum", Type.STRING},
			{"cang", "dacp.nowplayinggenre", Type.STRING},
			{"cant", "dacp.remainingtime", Type.INT},
			{"casp", "dacp.speakers", Type.CONTAINER},
			{"cass", "dacp.ss", Type.BYTE},
			{"cast", "dacp.tracklength", Type.INT},
			{"casu", "dacp.su", Type.BYTE},
			{"caSG", "dacp.sg", Type.BYTE},
			{"cacr", "dacp.cacr", Type.CONTAINER},
		
			{"cmcp", "dmcp.controlprompt", Type.CONTAINER},
			{"cmgt", "dmcp.getpropertyresponse", Type.CONTAINER},
			{"cmik", "dmcp.ik", Type.BYTE},
			{"cmsp", "dmcp.ik", Type.BYTE},
			{"cmst", "dmcp.status", Type.CONTAINER},
			{"cmsv", "dmcp.sv", Type.BYTE},
			{"cmsr", "dmcp.mediarevision", Type.INT},
			{"cmmk", "dmcp.mediakind", Type.INT},
			{"cmvo", "dmcp.volume", Type.INT}

		};
		
		/*public static void initiliaze() {
			if (cc_names == null)
				cc_names = new GLib.HashTable<string,unowned ContentCode?>(str_hash, str_equal);
		}*/

		/*
		public static void add(string code, string? name, Type type) {
			initiliaze();
			unowned ContentCode cc = {code, name, type};
			if (name != null)
				cc_names.insert(name, cc);
		}*/
		
		public static unowned ContentCode? lookup_by_code(string code) {
			//unowned ContentCode? content_code = cc_names.find((name, cc) => {
			//	return (cc.code == code);
			//});
			//if (content_code != null)
			//	return content_code;
			foreach (unowned ContentCode cc in CODES) {
				if (cc.code == code)
					return cc;
				//cc_names.insert(cc.name, cc);
			}
			return null;
		}
		
		public static unowned ContentCode? lookup_by_name(string name) {
			//initiliaze();
			//debug("Looking up code %s", name);
			//ContentCode? cc = cc_names.lookup(name);
			//if (cc == null) {
				foreach (unowned ContentCode code in CODES) {
					if (code.name == name) {
						//debug("Found code %s, type %d", code.code, code.type);
						//cc = code;
						return code;
						//break;
					}
				}
				return null;
			//	cc_names.insert(name, cc);
			//}
			//debug("Found code %s, type %d", cc.code, cc.type);
			//return cc;
		}
	}
		
	public errordomain Serialize.Error {
		SERIALIZE_WRONG_VALUE,
		CODE_NOT_FOUND,
		UNSUPPORTED_TYPE,
		UNEXPECTED_DATA_END
	}
	
	public delegate void ContainerCallback(Structure node) throws Serialize.Error;
	
	public abstract class SerializableObject : Object {

		//public ContentCode? content_code = null;

		public Gee.ArrayList<unowned ContentCode?> _serializables;

		/*construct {
			content_code = ContentCode.lookup_by_name(this.get_code());
			warning("Object %s : %s", this.get_code(), content_code.code);
		}*/

		public virtual Gee.List<unowned ContentCode?> get_serializables() {
			//if (_serializables == null) {
				var _serializables = new Gee.ArrayList<ContentCode?>();

				var pspecs = this.get_class().list_properties();
				foreach (ParamSpec pspec in pspecs) {
					string[] known_prefix = {"dmap", "daap", "dacp", "dacp", "dpap", "com"};
					if (pspec.get_nick().split("-", 0)[0] in known_prefix ) {
						var cc = ContentCode.lookup_by_name(pspec.get_nick().replace("-", "."));
						if (cc != null)
							_serializables.add(cc);
					}
				}
				/*foreach (string property in this.get_properties()) {
					var cc = ContentCode.lookup_by_name(property);
					if (cc != null)
						_serializables.add(cc);
					else
						warning("Property %s has no code", property);
				}*/
			//}
			return _serializables;
		}

		public virtual Gee.List<string> get_properties() {
			return new Gee.LinkedList<string>();
		}

		public abstract string get_code();

		public virtual ContentCode get_content_code() {
			return ContentCode.lookup_by_name(this.get_code());
		}

		public virtual Gee.Iterable<Structure> get_serializeable_children() {
			var properties = new Gee.LinkedList<Structure>();
			///warning("Serializing children");
			//foreach (string property in this.get_properties())
			//	print("Property: %s\n", property);
			//print("-- Properties --\n");
			foreach (unowned ContentCode? prop in this.get_serializables()) {
				//if (prop )
				//warning("Property %s", prop.name);
				var pspec = this.get_class().find_property(prop.name);
				Value value = Value(pspec.value_type);
				this.get_property(prop.name, ref value);
				properties.add(new Structure.from_cc(prop, value));
			}
			return properties;
			//return new Structure(this.get_code(), null);
		}

		public virtual Structure serialize() {
			return new Structure.from_cc(this.get_content_code(), this);
		}
	}

	public class Listing : SerializableObject { 
		public ListingResponse parent;

		public Listing (ListingResponse parent) {
			this.parent = parent;
		}

		public override string get_code() {
			return "mlcl";
		}

		public override Gee.Iterable<Structure> get_serializeable_children() {
			var properties = new Gee.LinkedList<Structure>();
			var children = parent.get_children();
			foreach (ListingItem child in children) {
				properties.add(new Structure.from_cc(child.get_content_code(), child));
			}
			return properties;
		}
	}

	public class ListingItem : SerializableObject {
		public override string get_code() {
			return "mlit";
		}


		public int dmap_itemid { get; set; default = 0; }
		public int dmap_persistentid { get; set; default = 0; }
		public string dmap_itemname { get; set; default = ""; }
	}

	public interface ServerResponse : SerializableObject {
		public virtual int dmap_status { get; set; default = 200; }
	}

	public interface ListingResponse : ServerResponse {
		public int dmap_updatetype { get; set; default = 0; }

		public virtual int dmap_specifiedtotalcount { get; set; default = 0; }
		public virtual int dmap_returnedcount { get; set; default = 0; }
		public Listing dmap_listing { get; set; }

		public abstract Gee.Iterable<ListingItem> get_children();
	}

	public class Structure {
		private Gee.ArrayList<Structure> children;
		private Value? data;
		private ContentCode? cc;

		private static Gee.HashMap<string, ContentCode?> cc_names;
		private static Gee.HashMap<string, ContentCode?> cc_codes;

		static construct {
			cc_names = new Gee.HashMap<string, ContentCode?>();
			cc_codes = new Gee.HashMap<string, ContentCode?>();

			foreach (unowned ContentCode cc in ContentCode.CODES) {
				cc_names.set(cc.name, cc);
				cc_codes.set(cc.code, cc);
			}
		}
		
		public Structure(string? name, Value ?data) throws Serialize.Error {
			/*if (!cc_initialized) {
				ContentCode.initialize();
				cc_initialized = true;
			}*/
			ContentCode? cc;
			if (name.length == 4)
				cc = cc_codes.get(name);
			else
				cc = cc_names.get(name);
			if (cc == null)
				throw new Serialize.Error.CODE_NOT_FOUND ("Content code for %s not found", name);
			this.from_cc(cc, data);
		}
		
		public Structure.from_cc(ContentCode cc, Value ?data) {
			this.cc = cc;
			this.data = data;
			this.children = new Gee.ArrayList<Structure?>();
		}
		
		/*
		public Structure add(string? name, Value ?data) throws Serialize.Error {
			Structure node = new Structure(name, data);
			this.children.add(node);
			return node;
		}
		
		public Structure add_container(string? name, ContainerCallback callback) throws Serialize.Error {
			Structure node = add(name, null);
			callback(node);
			return node;
		}
		*/
		
		private Value convert_value(Value data, GLib.Type type) throws Serialize.Error {
			var dest = Value(type);
			if (data.type_transformable(data.type(), type)) {
				data.transform(ref dest);
				return dest;
			} else {
				throw new Serialize.Error.SERIALIZE_WRONG_VALUE("Wrong value");
			}
		}
		
		public bool serialize (OutputStream out_stream) {
			var data_stream = new DataOutputStream(out_stream);
			data_stream.set_close_base_stream(false);
			data_stream.set_byte_order(DataStreamByteOrder.BIG_ENDIAN);
			//uint8[] code = (uint8[]) this.cc.code.reverse();
			//data_stream.put_uint32((uint32)code);
			try {
				data_stream.put_uint32(this.cc.code[0] |
				  this.cc.code[1] << 8  |
				  this.cc.code[2] << 16 |
				  this.cc.code[3] << 24);
				switch (this.cc.type) {
					case Type.CONTAINER:
						var stream = new MemoryOutputStream(null, realloc, free);
						//this.children.reverse();
						var children = new Gee.ArrayList<Structure>();
						children.add_all(this.children);

						if (data.holds(typeof(Object)) && (Object) data is SerializableObject) {
							foreach (Structure child in (data as SerializableObject).get_serializeable_children()) {
								children.add(child);
							}
						}

						foreach (Structure child in children) {
							child.serialize(stream);
						}

						unowned uint8[] data_value = stream.get_data();
						/* Not sure if it's a Vala bug, but the array
						   length is not correctly set from get_data */
						data_value.length = (int32)stream.get_data_size();
						data_stream.put_int32((int32)stream.get_data_size());
						data_stream.write(data_value);
						break;
					case Type.BYTE:
					case Type.SIGNED_INT:
						data_stream.put_int32((int32)sizeof(char));
						data_stream.put_byte((char)this.convert_value(data, typeof(char)));
						break;
					case Type.SHORT:
						data_stream.put_int32(2);
						/* Probably a bug in Vala, without the 
						   cast it will try to convert the Value
						   directly */
						data_stream.put_int16((int16)(this.convert_value(data, typeof(int16)).get_int()));
						break;
					case Type.DATE:
					case Type.INT:
						data_stream.put_int32(4);
						data_stream.put_int32((int32)this.data);
						break;
					case Type.INT64:
						data_stream.put_int32(8);
						data_stream.put_int64((int64)this.data);
						break;
					case Type.STRING:
						string val = (string)this.data;
						data_stream.put_int32(val.length);
						data_stream.put_string(val);
						break;
					case Type.VERSION:
						string val = (string)this.data;
						data_stream.put_int32(4);
						var version = val.split(".");
						data_stream.put_uint16((version.length > 0) ? (uint16) int.parse(version[0]) : 0);
						data_stream.put_byte((version.length > 1) ? (uint8) int.parse(version[1]) : 0);
						data_stream.put_byte((version.length > 2) ? (uint8) int.parse(version[2]) : 0);
						break;
					default:
						//warning("Unsupported type");
						throw new Serialize.Error.UNSUPPORTED_TYPE("Unsupported type (%d) for property %s", this.cc.type, this.cc.name);
				}
			} catch (Serialize.Error e) {
				warning("Unable to serialize structure: %s", e.message);
				return false;
			} catch (IOError e) {
				warning("Could not write serialized structure: %s", e.message);
				return false;
			}
			return true;
		}
		
		public static Structure? parse(InputStream in_stream) throws Serialize.Error {
			DataInputStream data = in_stream as DataInputStream;
			if (data == null) {
				data = new DataInputStream(in_stream);
			};
			data.set_byte_order(DataStreamByteOrder.BIG_ENDIAN);
			
			string code;
			int length;
			try {
				uint8[4] _code = new uint8[4];
				data.read(_code);
				code = (string) _code;
				code = code.reverse();
				length = data.read_int32();
				//GLib.print("code: %s, length: %d\n", (string)code, length);
			} catch (IOError e) {
				return null;
			}
			
			try {
				ContentCode? cc = ContentCode.lookup_by_code(code);
				if (cc == null) {
					warning("Content code '%s' not found", code);
					data.skip(length);
					return null;
				}
				if (cc.type == Type.CONTAINER) {
					int64 start = (data.base_stream as Seekable).tell() - data.get_available();
					int64 pos = start;
					Structure root = new Structure(code, null);
					while (pos - start < length) {
						Structure? child = Structure.parse(data);
						if (child == null)
							break;
						root.children.add(child);
						pos = (data.base_stream as Seekable).tell() - data.get_available();
					}
					return root;
				} else {
					Value? val = null;
					switch (cc.type) {
						case Type.BYTE:
						case Type.SIGNED_INT:
							val = (char)data.read_byte();
							break;
						case Type.SHORT:
							val = Value(typeof(int16));
							val.set_int(data.read_int16());
							break;
						case Type.DATE:
						case Type.INT:
							val = data.read_int32();
							break;
						case Type.INT64:
							val = data.read_int64();
							break;
						case Type.STRING:
							uint8[] string_data = new uint8[length];
							data.read(string_data);
							val = (string) string_data;
							break;
						default:
							data.skip(length);
							warning("Unsupported type (%d), skipping", cc.type);
							break;
					}
					return new Structure(code, val);
				}
			} catch (IOError e) {
				throw new Serialize.Error.UNEXPECTED_DATA_END("Data ended unexpectedly");
			}
		}
		
		/*public Structure? find_by_code(string code) {
			foreach (Structure child in this.children) {
				if (child.cc.code == code)
					return child;
				Structure? structure = child.find_by_code(code);
				if (structure != null)
					return structure;
			}
			return null;
		}
		
		public Structure? find_by_name(string name) {
			foreach (Structure child in this.children) {
				if (child.cc.name == name)
					return child;
				Structure? structure = child.find_by_name(name);
				if (structure != null)
					return structure;
			}
			return null;
		}*/
		
		public void print() {
			this.print_depth(1);
		}
		
		private void print_depth(int depth) {
			for (int i = 1; i < depth; i++)
				GLib.print ("\t");
			if (this.data != null) {
				//Value data = Value(typeof(string));
				//this.data.transform(ref data);
				GLib.print ("%s = %s\n", this.cc.name, this.data.strdup_contents());
			} else
				GLib.print ("%s:\n", this.cc.name);
			
			var children = new Gee.ArrayList<Structure>();
			children.add_all(this.children);

			if (data.holds(typeof(Object)) && (Object)data is SerializableObject) {
				foreach (Structure child in ((SerializableObject)data).get_serializeable_children()) {
					children.add(child);
				}
			} 

			foreach (Structure child in children) {
				child.print_depth(depth + 1);
			}
		}
	}
}